Узнайте, как эффективно тестировать ваши приложения FastAPI с помощью TestClient. Охватывает лучшие практики, передовые методы и примеры из реального мира для надежных API.
Освоение тестирования FastAPI: Подробное руководство по TestClient
FastAPI стал ведущим фреймворком для создания высокопроизводительных API на Python. Его скорость, простота использования и автоматическая проверка данных делают его фаворитом среди разработчиков по всему миру. Однако хорошо построенный API хорош настолько, насколько хороши его тесты. Тщательное тестирование гарантирует, что ваш API функционирует должным образом, остается стабильным под нагрузкой и может быть уверенно развернут в рабочей среде. Это подробное руководство посвящено использованию TestClient FastAPI для эффективного тестирования конечных точек вашего API.
Почему тестирование важно для приложений FastAPI?
Тестирование является важным этапом жизненного цикла разработки программного обеспечения. Это помогает вам:
- Выявлять ошибки на ранней стадии: обнаруживайте ошибки до того, как они попадут в рабочую среду, экономя время и ресурсы.
- Обеспечивать качество кода: продвигайте хорошо структурированный и поддерживаемый код.
- Предотвращать регрессии: гарантируйте, что новые изменения не нарушат существующую функциональность.
- Повышать надежность API: укрепляйте уверенность в стабильности и производительности API.
- Облегчать сотрудничество: предоставляйте четкую документацию об ожидаемом поведении для других разработчиков.
Представляем TestClient FastAPI
FastAPI предоставляет встроенный TestClient, который упрощает процесс тестирования конечных точек вашего API. TestClient действует как облегченный клиент, который может отправлять запросы к вашему API без запуска полноценного сервера. Это делает тестирование значительно быстрее и удобнее.
Основные возможности TestClient:
- Имитирует HTTP-запросы: позволяет отправлять GET, POST, PUT, DELETE и другие HTTP-запросы к вашему API.
- Обрабатывает сериализацию данных: автоматически сериализует данные запроса (например, полезные нагрузки JSON) и десериализует данные ответа.
- Предоставляет методы утверждений: предлагает удобные методы для проверки кода состояния, заголовков и содержимого ответов.
- Поддерживает асинхронное тестирование: беспрепятственно работает с асинхронной природой FastAPI.
- Интегрируется с платформами тестирования: легко интегрируется с популярными платформами тестирования Python, такими как pytest и unittest.
Настройка среды тестирования
Прежде чем начать тестирование, вам необходимо настроить среду тестирования. Обычно это включает в себя установку необходимых зависимостей и настройку платформы тестирования.
Установка
Во-первых, убедитесь, что у вас установлены FastAPI и pytest. Вы можете установить их с помощью pip:
pip install fastapi pytest httpx
httpx — это HTTP-клиент, который FastAPI использует под капотом. Хотя TestClient является частью FastAPI, наличие установленного httpx также обеспечивает плавное тестирование. В некоторых учебных пособиях также упоминается requests, однако httpx больше соответствует асинхронной природе FastAPI.
Пример приложения FastAPI
Давайте создадим простое приложение FastAPI, которое мы сможем использовать для тестирования:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Сохраните этот код как main.py. Это приложение определяет три конечные точки:
/: простой GET-запрос, который возвращает сообщение «Hello World»./items/{item_id}: GET-запрос, который возвращает элемент на основе его идентификатора./items/: POST-запрос, который создает новый элемент.
Написание первого теста
Теперь, когда у вас есть приложение FastAPI, вы можете начать писать тесты, используя TestClient. Создайте новый файл с именем test_main.py в том же каталоге, что и main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
В этом тесте:
- Мы импортируем
TestClientи экземплярappFastAPI. - Мы создаем экземпляр
TestClient, передаваяapp. - Мы определяем тестовую функцию
test_read_root. - Внутри тестовой функции мы используем
client.get("/")для отправки GET-запроса в корневую конечную точку. - Мы утверждаем, что код состояния ответа равен 200 (OK).
- Мы утверждаем, что JSON-ответ равен
{"message": "Hello World"}.
Запуск тестов с помощью pytest
Чтобы запустить тесты, просто откройте терминал в каталоге, содержащем файл test_main.py, и выполните следующую команду:
pytest
pytest автоматически обнаружит и запустит все тесты в вашем проекте. Вы должны увидеть вывод, подобный этому:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Тестирование различных HTTP-методов
TestClient поддерживает все стандартные HTTP-методы, включая GET, POST, PUT, DELETE и PATCH. Давайте посмотрим, как протестировать каждый из этих методов.
Тестирование GET-запросов
Мы уже видели пример тестирования GET-запроса в предыдущем разделе. Вот еще один пример, тестирующий конечную точку /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Этот тест отправляет GET-запрос на /items/1 с параметром запроса q=test. Затем он утверждает, что код состояния ответа равен 200 и что JSON-ответ содержит ожидаемые данные.
Тестирование POST-запросов
Чтобы протестировать POST-запрос, необходимо отправить данные в теле запроса. TestClient автоматически сериализует данные в JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
В этом тесте:
- Мы создаем словарь
item_data, содержащий данные для нового элемента. - Мы используем
client.post("/items/", json=item_data)для отправки POST-запроса в конечную точку/items/, передаваяitem_dataв качестве полезной нагрузки JSON. - Мы утверждаем, что код состояния ответа равен 200 и что JSON-ответ соответствует
item_data.
Тестирование PUT, DELETE и PATCH-запросов
Тестирование PUT, DELETE и PATCH-запросов аналогично тестированию POST-запросов. Вы просто используете соответствующие методы в TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
Не забудьте добавить утверждения, чтобы убедиться, что ответы соответствуют ожиданиям.
Расширенные методы тестирования
TestClient предлагает несколько расширенных функций, которые могут помочь вам писать более полные и эффективные тесты.
Тестирование с зависимостями
Система внедрения зависимостей FastAPI позволяет легко внедрять зависимости в конечные точки вашего API. При тестировании может потребоваться переопределить эти зависимости, чтобы предоставить фиктивные или специфичные для теста реализации.
Например, предположим, что ваше приложение зависит от подключения к базе данных. Вы можете переопределить зависимость базы данных в своих тестах, чтобы использовать базу данных в памяти:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
В этом примере переопределяется зависимость get_db тестовой функцией, которая возвращает сеанс для базы данных SQLite в памяти. Важно: Создание метаданных должно быть явно вызвано для правильной работы тестовой базы данных. Невозможность создать таблицу приведет к ошибкам, связанным с отсутствующими таблицами.
Тестирование асинхронного кода
FastAPI построен как асинхронный, поэтому вам часто нужно будет тестировать асинхронный код. TestClient поддерживает асинхронное тестирование без проблем.
Чтобы протестировать асинхронную конечную точку, просто определите свою тестовую функцию как async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Примечание: Вам необходимо установить pytest-asyncio для использования @pytest.mark.asyncio: pip install pytest-asyncio. Вам также необходимо убедиться, что asyncio.get_event_loop() настроен, если вы используете более старые версии pytest. При использовании pytest версии 8 или новее это может не потребоваться.
Тестирование загрузки файлов
FastAPI упрощает обработку загрузки файлов. Чтобы протестировать загрузку файлов, вы можете использовать параметр files методов запроса TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
В этом тесте мы создаем фиктивный файл с помощью io.BytesIO и передаем его в параметр files. Параметр files принимает список кортежей, где каждый кортеж содержит имя поля, имя файла и содержимое файла. Тип контента важен для точной обработки сервером.
Тестирование обработки ошибок
Важно протестировать, как ваш API обрабатывает ошибки. Вы можете использовать TestClient для отправки недействительных запросов и проверки того, что API возвращает правильные ответы об ошибках.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Этот тест отправляет GET-запрос на /items/101, который вызывает HTTPException с кодом состояния 400. Тест утверждает, что код состояния ответа равен 400 и что JSON-ответ содержит ожидаемое сообщение об ошибке.
Тестирование функций безопасности
Если ваш API использует аутентификацию или авторизацию, вам также необходимо протестировать эти функции безопасности. TestClient позволяет устанавливать заголовки и файлы cookie для имитации аутентифицированных запросов.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
В этом примере мы тестируем конечную точку входа и затем используем полученный токен для доступа к защищенному маршруту. Параметр headers методов запроса TestClient позволяет устанавливать пользовательские заголовки, включая заголовок Authorization для токенов носителя.
Рекомендации по тестированию FastAPI
Вот несколько рекомендаций, которым следует следовать при тестировании приложений FastAPI:
- Пишите полные тесты: стремитесь к высокому покрытию тестами, чтобы гарантировать тщательное тестирование всех частей вашего API.
- Используйте описательные имена тестов: убедитесь, что имена ваших тестов четко указывают, что именно проверяет тест.
- Следуйте шаблону Arrange-Act-Assert: организуйте свои тесты в три отдельные фазы: Arrange (настройка тестовых данных), Act (выполнение тестируемого действия) и Assert (проверка результатов).
- Используйте фиктивные объекты: высмеивайте внешние зависимости, чтобы изолировать свои тесты и избежать зависимости от внешних систем.
- Тестируйте крайние случаи: протестируйте свой API с недействительными или неожиданными входными данными, чтобы убедиться, что он корректно обрабатывает ошибки.
- Часто запускайте тесты: интегрируйте тестирование в свой рабочий процесс разработки, чтобы выявлять ошибки на ранней стадии и часто.
- Интеграция с CI/CD: автоматизируйте свои тесты в конвейере CI/CD, чтобы убедиться, что все изменения кода тщательно протестированы перед развертыванием в рабочей среде. Для достижения этой цели можно использовать такие инструменты, как Jenkins, GitLab CI, GitHub Actions или CircleCI.
Пример: тестирование интернационализации (i18n)
При разработке API для глобальной аудитории важна интернационализация (i18n). Тестирование i18n включает в себя проверку того, что ваш API правильно поддерживает несколько языков и регионов. Вот пример того, как вы можете протестировать i18n в приложении FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
В этом примере заголовок Accept-Language устанавливается для указания желаемого языка. API возвращает приветствие на указанном языке. Тестирование гарантирует, что API правильно обрабатывает различные языковые предпочтения. Если заголовок Accept-Language отсутствует, используется язык по умолчанию «en».
Заключение
Тестирование — важная часть создания надежных и стабильных приложений FastAPI. TestClient предоставляет простой и удобный способ тестирования конечных точек вашего API. Следуя рекомендациям, изложенным в этом руководстве, вы можете писать полные тесты, которые обеспечат качество и стабильность ваших API. От базовых запросов до передовых методов, таких как внедрение зависимостей и асинхронное тестирование, TestClient позволяет вам создавать хорошо протестированный и поддерживаемый код. Включите тестирование в качестве основной части вашего рабочего процесса разработки, и вы создадите API, которые будут одновременно мощными и надежными для пользователей по всему миру. Помните о важности интеграции CI/CD для автоматизации тестирования и обеспечения непрерывного обеспечения качества.